Skip to content

Conversation

@ouillie
Copy link
Contributor

@ouillie ouillie commented Nov 5, 2025

There was an issue with the profiling logic in the unifier in the web environment. I guess Wasm core modules don't get clock access, which was causing a panic. I added a feature flag to gate the feature, where previously you were using if false. Hopefully all my edits there make sense.

The demo works now, at least for basic statements like val x = 10. Seeing if I can run all the unit tests with wasm-bindgen-test.

For the record, I used console_error_panic_hook to debug the panic, and it worked like a charm. See this example. I was surprised to see a lot of the rustwasm stuff was recently sunset, but I suppose it was always a stopgap measure until proper Component Model support arrives for the browser. Then, you wouldn't need wasm-bindgen or wasm-pack anymore.

Some things that took me a weirdly long time to learn about Rust:

  • Rust has eprintln! in addition to println!. The former goes to stderr instead of stdout. I feel like that's probably what you want to use in most of the places where you use println!.
  • You might benefit from using thiserror or snafu instead of custom enums to define and handle your errors. See also this discussion (unless you have a life, I guess)

Fixes #13

julianhyde and others added 3 commits November 4, 2025 21:59
Fixes hydromatic#13

To make it easy to call Morel from WASM, we need a simple
interface to the shell. We already have `process_line`,
which takes a string and returns a string. It is stateful,
so variables assigned in one statement are available in the
next statement:

```sml
  let mut shell = Shell::new(&[]);
  let mut result;

  let in_1 = "val x = 5\n\
      and y = 6\n";
  let out_1 = "> val x = 5 : int\n\
      > val y = 6 : int\n";
  result = shell.process_statement(in_1, None).unwrap();
  assert_eq!(result, out_1);

  let in_2 = "x + y\n";
  let out_2 = "> val it = 11 : int\n";
  result = shell.process_statement(in_2, None).unwrap();
  assert_eq!(result, out_2);
```

The current API is string-in, string-out. The input must
be a single declaration or expression (without a
semicolon). The output prints a value and its type. Future
enhancements could be multi-statement input (separated by
semicolons) and output as a structured value.
@julianhyde
Copy link
Contributor

Looks great! I got the server running.

In https://github.com/julianhyde/morel-rust/tree/0013-browser I added another couple of commits (enabling linting, and adding a favicon). Can you please review? If you're Ok I'll squash and merge to main.

I had a couple of problems, as you can see in this screenshot. I plan to fix these in follow up commits.

Screenshot 2025-11-12 at 6 12 03 PM

I also plan to add banner and version properties, that you can access like this:

Sys.show "productName";
val it = "morel-rust" : string option
Sys.show "productVersion";
val it = "0.3" : string option
Sys.show "banner";
val it = "morel-rust version 0.2.0 (wasm x.y rust version 1.91.0)" : string option

Any ideas what I should replace 'wasm x.y' with, and how the wasm runtime can get that information?

@julianhyde
Copy link
Contributor

I logged hydromatic/morel#319 for the properties.

@ouillie
Copy link
Contributor Author

ouillie commented Nov 13, 2025

You can add your commits directly to this PR if you push them to the main branch on my personal fork. I believe you can do that as long as this PR is open because I checked the "Allow edits by maintainers" box when I opened it. Then I could add my comments to your commits using the regular GH review tools.

That said, your commits seems pretty focused and entirely unobjectionable :).

I'm still too much of a Morel novice to spot the issues in that screenshot. Is it specific to the Wasm build?

Regarding the runtime, that's up to the user (V8 for Chrome, SpiderMonkey for FF, etc.) but there are a bunch of stats you can use to describe the module itself.

TL;DR perhaps calling it wasm32 core 2.0/3.0 makes most sense here.

It's a core Wasm module (as opposed to a component module). That means it deals exclusively in numeric primitives (just 8, 16, 32, or 64-bit integers or 32 or 64-bit floats) and linear memory (just a big byte array you can index into), so things like strings have to be modeled in a Rust-native way (pointer+length). When JS passes a string to a Rust function, it first encodes the UTF-16 JS string as a UTF-8 Rust string, then allocates space in the module's linear memory using a special function exported by the module, then writes the UTF-8 bytes into that space, then passes the pointer+length to the Rust function its invoking. It's all very low-level and language-specific, which is why the JS glue is necessary.

The 32 in wasm32 means that the linear memories use 32-bit indices. There's also wasm64 but it's newer and has way less adoption right now.

You can verify that it's a core Wasm module with wasm-tools e.g.

$ wasm-tools metadata show target/wasm32-unknown-unknown/release/morel.wasm
╭────────┬────────────┬──────┬───────┬───────────┬────────╮
│ KIND   ┆ NAME       ┆ SIZE ┆ SIZE% ┆ LANGUAGES ┆ PARENT │
╞════════╪════════════╪══════╪═══════╪═══════════╪════════╡
│ module ┆ morel.wasm ┆ 2.7M ┆  100% ┆ Rust      ┆ <root> │
╰────────┴────────────┴──────┴───────┴───────────┴────────╯
╭──────────────┬───────────────────────────────────────╮
│ KIND         ┆ VALUE                                 │
╞══════════════╪═══════════════════════════════════════╡
│ name         ┆ morel.wasm                            │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ kind         ┆ module                                │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ range        ┆ 0x0..0x298397                         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ language     ┆ Rust                                  │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ processed-by ┆ rustc [1.91.0 (f8297e351 2025-10-28)] │
╰──────────────┴───────────────────────────────────────╯

If there were a component in that file, it would have shown up with kind component instead of just module.

This language-specificity is the main motivation for the Component Model, which aims to formalize the system of glue code into standardized "lifting" and "lowering" logic exported by the modules to convert native representations to canonical representations and back, allowing them to share things like strings, lists, structs, etc. across language boundaries in a much more natural way. It's an extra layer of rich typing, on top of the core modules that implement all the actual logic but deal exclusively in primitives and have language-specific representations for everything.

Pretty much all the metadata from the module is stored in custom sections. Use wasm-tools to get an overview of all the sections in the module:

$ wasm-tools objdump target/wasm32-unknown-unknown/release/morel.wasm
  types                                  |        0xb -      0x17d |       370 bytes | 40 count
  imports                                |      0x180 -      0x385 |       517 bytes | 9 count
  functions                              |      0x388 -      0xc32 |      2218 bytes | 2216 count
  tables                                 |      0xc34 -      0xc3b |         7 bytes | 1 count
  memories                               |      0xc3d -      0xc40 |         3 bytes | 1 count
  globals                                |      0xc42 -      0xc5b |        25 bytes | 3 count
  exports                                |      0xc5e -     0x1bcc |      3950 bytes | 72 count
  elements                               |     0x1bcf -     0x2000 |      1073 bytes | 1 count
  code                                   |     0x2005 -   0x23bef9 |   2334452 bytes | 2216 count
  data                                   |   0x23befd -   0x2616e8 |    153579 bytes | 2 count
  custom "__wasm_bindgen_unstable"       |   0x261703 -   0x263284 |      7041 bytes | 1 count
  custom "name"                          |   0x26328d -   0x2982b1 |    217124 bytes | 1 count
  custom "producers"                     |   0x2982bd -   0x298300 |        67 bytes | 1 count
  custom "target_features"               |   0x298313 -   0x298397 |       132 bytes | 1 count

Some of these custom sections are standardized, like the name section which holds unmangled names and debugging symbols. All of these custom sections can simply be removed from the module using wasm-tools strip without affecting the behavior (although I'm not totally sure about __wasm_bindgen_unstable). The target_features section basically describes certain proposals it needs support for. That's probably too fine-grained information to be relevant here.

Finally, there are 3 major versions of the Wasm spec. You can validate it against each version:

$ wasm-tools validate target/wasm32-unknown-unknown/release/morel.wasm --features=wasm1
$ wasm-tools validate target/wasm32-unknown-unknown/release/morel.wasm --features=wasm2
$ wasm-tools validate target/wasm32-unknown-unknown/release/morel.wasm --features=wasm3

I found that it's valid against 2.0 and 3.0, but not 1.0.

Also, it's built using the web target for wasm-pack. This only affects the surrounding JS glue code, not the binary itself. It means the glue implements an ES6 module that can be imported on a web page. Other targets include bundler (no idea how that works) or nodejs (which would make use of Node-specific APIs like fs).

If you wanted to build this library as a component instead of a core module, you could use rules_wasm (disclaimer: I wrote it). It would involve first describing the interface as a WIT file, then generating Rust bindings from that WIT interface, then compiling it all together. I'm biased, but I think my Bazel ruleset is the easiest way to do that (as long as you're OK with using Bazel). Then you could run it locally using wasmtime, which is essentially the reference runtime, and has more features than any of the Browser runtimes.

@julianhyde
Copy link
Contributor

I'm still too much of a Morel novice to spot the issues in that screenshot. Is it specific to the Wasm build?

Ah, sorry, I posted the wrong screenshot. The errors were because I gave a statement with a syntax error:

$ from i [1,2]
Error: RuntimeError: unreachable

and then (understandably) the environment seems to be hosed for the next statement:

$ from i in [1,2]
Error: Error: recursive use of an object detected which would lead to unsafe aliasing in rust

I'll log a bug for error-handling.

By the way, I like your suggestions about eprintf and snafu/thiserror. I'll get to those too.

Thank you for your exhaustive explanations of WASM and its toolchain. I know I will come back to your comments when I hit problems in future.

You can add your commits directly to this PR if you push them to the main branch on my personal fork.

Nice idea. Curiously, I've never done this. My personal style is to accept the PR, rework it a bit if necessary (as a copy-editor would rework a newspaper article), and then move on. If it's good enough for main branch, put it in main branch, and there can be follow-on PRs.

I'll merge this work shortly.

Seeing Morel queries and their output appear in a web browser has got me excited. For example, I could finally work through an example program that compute K-means clusters (see hydromatic/morel#294) and display the clusters -- say the latitude/longitude of US state capitals -- as 2-d plots. We finally escape the bounds of textual output.

@ouillie
Copy link
Contributor Author

ouillie commented Nov 13, 2025

FYI this guy does a way better job of doing an intro to wasm-tools than I can: https://www.youtube.com/watch?v=9O5NNOUuHPI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Morel in the browser

2 participants